package ee.logsimulator.ui;

import ee.logsimulator.Model;
import ee.logsimulator.data.Actor;
import ee.logsimulator.data.ActorEvents;
import ee.logsimulator.data.Amount;
import ee.logsimulator.data.Event;
import ee.logsimulator.data.Spell;

import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.ScrollPane;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.swing.JSlider;

import org.apache.commons.lang.StringUtils;

public abstract class AbstractPlayer extends Canvas implements Runnable {

  private static final long serialVersionUID = 1L;
  private static final int SLEEPTIME = 8;
  protected static final int ACTOR_WIDTH = 200;
  protected static final int ACTOR_HEIGHT = 200;
  protected static final double MILLIS_PER_PIXEL = 0.126;

  protected Model model;
  protected JSlider slider;
  private Thread thread;
  protected ScrollPane scroll;

  protected int time = -1;
  protected boolean pixelsCalculated = false;
  private volatile long resetTime = 0;
  protected long timeStart;
  private volatile boolean running = false;
  protected volatile BufferedImage i;
  protected List<ActorEvents> actorEvents;

  public AbstractPlayer(Model model) {
    this.model = model;

    timeStart = model.getFirst().getTimestamp().getTime();
    drawTimeline();
    int reqWidth = Math.abs(2 * (actorEvents.get(0).getPosition() * (ACTOR_WIDTH + 10) + 5));
    setSize(new Dimension(reqWidth, getHeight()));
    update();
  }

  protected void fillEventPixels(Graphics g) {
    pixelsCalculated = true;
    setTimelineFont(g);
    FontMetrics metrics = g.getFontMetrics();
    setTimelineBoldFont(g);
    FontMetrics boldMetrics = g.getFontMetrics();
    for (ActorEvents ae : actorEvents) {
      int drawnTo = Integer.MIN_VALUE;
      int lag = 0;
      for (Event e : ae.getEvents()) {
        long relTime = e.getTimestamp().getTime() - model.getFirst().getTimestamp().getTime();
        int pixelTime = (int) Math.round(relTime * MILLIS_PER_PIXEL);
        FontMetrics m = e.getAmount().isCrit() ? boldMetrics : metrics;
        if (pixelTime - m.getHeight() < drawnTo) {
          lag = drawnTo - (pixelTime - m.getHeight());
        } else if (lag != 0) {
          lag = 0;
        }
        pixelTime = pixelTime + lag;
        drawnTo = pixelTime;
        e.setPixelTime(pixelTime);
      }
    }
  }

  @Override
  public Dimension getPreferredSize() {
    if (scroll != null) {
      int h = scroll.getHeight();
      h -= 24;
      return new Dimension(getWidth(), h);
    } else {
      return super.getPreferredSize();
    }
  }

  @Override
  public void update(Graphics graphics) {
    paint(graphics);
  }

  protected String shoertenSpellTo(String spellName, FontMetrics metrics, int maxLenght) {
    if (spellName.length() < 6 || metrics.stringWidth(spellName) < maxLenght) {
      return spellName;
    }
    String substring = spellName.substring(0, spellName.length() - 5) + "...";
    if (metrics.stringWidth(substring) > maxLenght) {
      substring = shoertenSpellTo(substring, metrics, maxLenght);
    }
    return substring;
  }

  protected String abrevateNumber(Integer nr, int minIntegerDigits, int maxIntegerDigits, int minFractionDigits) {
    NumberFormat f = DecimalFormat.getNumberInstance();
    f.setMinimumIntegerDigits(minIntegerDigits);
    f.setMaximumIntegerDigits(maxIntegerDigits);
    f.setMaximumFractionDigits(minFractionDigits);
    if (nr < 1000) {
      return nr.toString();
    } else if (nr < 10000) {
      return f.format(nr / 1000.0) + "k";
    } else if (nr < 1000000) {
      return f.format(nr / 1000.0) + "k";
    } else if (nr < 1000000000) {
      return f.format(nr / 1000000.0) + "m";
    }
    return f.format(nr);
  }

  protected List<Entry<Spell, Integer>> sortEntrySet(Map<Spell, Integer> damageDone) {
    List<Entry<Spell, Integer>> list = new ArrayList<Entry<Spell, Integer>>(damageDone.entrySet());
    Collections.sort(list, new Comparator<Entry<Spell, Integer>>() {
      @Override
      public int compare(Entry<Spell, Integer> o1, Entry<Spell, Integer> o2) {
        if (o1.getValue().equals(o2.getValue())) {
          return o1.getKey().getName().compareTo(o2.getKey().getName());
        }
        return o2.getValue().compareTo(o1.getValue());
      }
    });
    return list;
  }

  protected void readEvents(long until, long drawFrom, List<Event> list, Map<Spell, Integer> damageDone, List<Event> toDraw) {
    Spell sp = new Spell(" Total");
    for (Event e : list) {
      long t = e.getTimestamp().getTime();
      if (t > until) {
        break;
      }
      Integer damage = damageDone.get(e.getSpell());
      Integer total = damageDone.get(sp);
      if (damage == null) {
        damage = 0;
      }
      if (total == null) {
        total = 0;
      }
      if (resetTime < t) {
        damageDone.put(e.getSpell(), damage + e.getAmount().getAmount());
        damageDone.put(sp, total + e.getAmount().getAmount());
      }
      if (t >= drawFrom) {
        toDraw.add(e);
      }
    }
  }

  @Override
  public void paintAll(Graphics g) {
    paint(g);
  }

  protected Font setTimelineExtraFont(Graphics g) {
    Font font = g.getFont();
    font = font.deriveFont(12f);
    font = font.deriveFont(Font.PLAIN);
    g.setFont(font);
    return font;
  }

  protected String getAmountExtra(Amount a) {
    List<String> extra = new ArrayList<String>();
    if (a.isOverkill()) {
      extra.add(abrevateNumber(a.getOverkill(), 1, 3, 0));
    }
    if (a.isOverheal()) {
      extra.add(abrevateNumber(a.getOverheal(), 1, 3, 0));
    }
    if (a.isBlock()) {
      extra.add(abrevateNumber(a.getBlocked(), 1, 3, 0));
    }
    if (a.isAbsorb()) {
      extra.add(abrevateNumber(a.getAbsorbed(), 1, 3, 0));
    }
    if (a.isResist()) {
      extra.add(abrevateNumber(a.getResisted(), 1, 3, 0));
    }

    return extra.isEmpty() ? null : " (" + StringUtils.join(extra, " ") + ")";
  }

  protected Font setTimelineFont(Graphics g) {
    Font font = g.getFont();
    font = font.deriveFont(14f);
    font = font.deriveFont(Font.PLAIN);
    g.setFont(font);
    return font;
  }

  protected Font setTimelineBoldFont(Graphics g) {
    Font font = g.getFont();
    font = font.deriveFont(14f);
    font = font.deriveFont(Font.BOLD);
    g.setFont(font);
    return font;
  }

  public void update() {
    repaint();
  }

  private void drawTimeline() {
    actorEvents = new ArrayList<ActorEvents>();
    int startTimes = 0 - (int) Math.round(Math.ceil(model.getPlayerEvents().keySet().size() / 2.0));
    List<Entry<Actor, List<Event>>> list = new ArrayList<Entry<Actor, List<Event>>>(model.getPlayerEvents().entrySet());
    Collections.sort(list, new Comparator<Entry<Actor, List<Event>>>() {
      @Override
      public int compare(Entry<Actor, List<Event>> o1, Entry<Actor, List<Event>> o2) {
        Integer o1Amount = 0;
        Date o1Last = null;
        for (Event e : o1.getValue()) {
          o1Amount += e.getAmount().getAmount();
          o1Last = e.getTimestamp();
        }
        Integer o2Amount = 0;
        Date o2Last = null;
        for (Event e : o2.getValue()) {
          o2Amount += e.getAmount().getAmount();
          o2Last = e.getTimestamp();
        }
        if (!o1Amount.equals(o2Amount)) {
          return o2Amount.compareTo(o1Amount);
        }
        if (!o1Last.equals(o2Last)) {
          return o2Last.compareTo(o1Last);
        }
        return o1.getKey().getName().compareTo(o2.getKey().getName());
      }
    });

    for (Entry<Actor, List<Event>> entry : list) {
      ActorEvents actorEvent = new ActorEvents(entry.getKey(), entry.getValue(), startTimes);
      actorEvents.add(actorEvent);
      startTimes++;
    }
  }

  public void play() {
    if (thread == null) {
      thread = new Thread(this);
      thread.start();
    } else {
      running = true;
    }
  }

  public void pause() {
    running = false;
  }

  public void reset(long time) {
    resetTime = timeStart + time;
  }

  @Override
  public void run() {

    running = true;
    int curValue = slider.getValue();
    while (curValue < slider.getMaximum()) {
      if (running) {
        curValue = slider.getValue();
        slider.setValue(curValue + SLEEPTIME);
      }
      try {
        Thread.sleep(SLEEPTIME);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    running = false;
    thread = null;
  }

  public void setSlider(JSlider slider) {
    this.slider = slider;
  }

  public void setScroll(ScrollPane scroll) {
    this.scroll = scroll;
  }

}
